/*
 * Copyright (c) 2024, gaoweixuan (breeze-cloud@foxmail.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.breeze.boot.security.sso.client.controller;

import cn.dev33.satoken.util.SaResult;
import cn.hutool.core.util.StrUtil;
import com.alibaba.druid.support.json.JSONUtils;
import com.breeze.boot.core.utils.BreezeTenantHolder;
import com.breeze.boot.core.utils.Result;
import com.breeze.boot.security.sso.client.security.exception.BizException;
import com.breeze.boot.security.sso.client.security.jwt.BreezeJwsTokenProvider;
import com.breeze.boot.security.sso.client.util.SsoRequestUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import static com.breeze.boot.core.constants.CoreConstants.X_TENANT_ID;

/**
 * SSO Client端 Controller
 *
 * @author gaoweixuan
 * @since 2024/09/24
 */
@Slf4j
@RestController
@RequiredArgsConstructor
public class SsoClientController {

    private final BreezeJwsTokenProvider jwsTokenProvider;
    private final RedisTemplate<String, Object> redisTemplate;

    /**
     * SSO-Client端：首页
     */
    @RequestMapping("/")
    public String index() {
        return "仅支持前后端分离";
    }

    /**
     * SSO-Client端：单点登录地址
     *
     * @param ticket   票
     * @param back     back
     * @param request  请求
     * @param response 响应
     * @return {@link Object }
     */
    @RequestMapping("/sso/login")
    public Object ssoLogin(String ticket, @RequestParam(defaultValue = "/") String back, HttpServletRequest request, HttpServletResponse response) {
        try {
            // 如果已经登录，则直接返回
            String satoken = jwsTokenProvider.getTokenStr(request);
            if (satoken != null) {
                jwsTokenProvider.verifyHMACToken(satoken);
                response.sendRedirect(back);
                return null;
            }

            /*
             * 此时有两种情况:
             * 情况1：ticket无值，说明此请求是Client端访问，需要重定向至SSO认证中心
             * 情况2：ticket有值，说明此请求从SSO认证中心重定向而来，需要根据ticket进行登录，改为前后端分离模式
             */
            if (ticket == null) {
                String currUrl = request.getRequestURL().toString();
                String tenantId = String.valueOf(BreezeTenantHolder.getTenant());
                String clientLoginUrl = currUrl + "?back=" + SsoRequestUtil.encodeUrl(back) + "&" + X_TENANT_ID + "=" + tenantId;
                String serverAuthUrl = SsoRequestUtil.authUrl + "?redirect=" + clientLoginUrl;
                response.sendRedirect(serverAuthUrl);
                return null;
            }
        } catch (Exception e) {
            throw new BizException("ticket=" + ticket);
        }
        return SaResult.ok();
    }

    /**
     * SSO-Client端：单点注销地址
     *
     * @param back     < 返回
     * @param response 响应
     * @param request  阶段
     * @return {@link Object }
     * @throws IOException IOException
     */
    @RequestMapping("/sso/logout")
    public Object ssoLogout(@RequestParam(defaultValue = "/") String back, HttpServletResponse response, HttpServletRequest request) throws IOException {
        // 如果未登录，则无需注销
        String satoken = request.getParameter("satoken");
        if (satoken == null) {
            response.sendRedirect(back);
            return null;
        }

        // 调用 sso-server 认证中心单点注销API
        Object loginId = jwsTokenProvider.getTokenClaim(satoken, "SSO_ID"); // 账号id
        String timestamp = String.valueOf(System.currentTimeMillis());    // 时间戳
        String nonce = SsoRequestUtil.getRandomString(20);        // 随机字符串
        String sign = SsoRequestUtil.getSignoutSign(loginId, timestamp, nonce);    // 参数签名

        String url = SsoRequestUtil.sloUrl
                + "?msgType=signout"
                + "&loginId=" + loginId
                + "&timestamp=" + timestamp
                + "&nonce=" + nonce
                + "&sign=" + sign
                + "&client=sso-client1";
        SaResult result = SsoRequestUtil.request(url);
        // 校验响应状态码，0000 代表成功
        if (result.getCode() == 200) {
            log.info("单点注销成功：{}", JSONUtils.toJSONString(result));
            // 极端场景下，sso-server 中心的单点注销可能并不会通知到此 client 端，所以这里需要再补一刀
            redisTemplate.opsForValue().set("security:jwt:blacklist:" + loginId + ":" + satoken, loginId, 24, TimeUnit.HOURS);
            // 返回 back 地址
            response.sendRedirect(back);
            return null;
        }
        // 将 sso-server 回应的消息作为异常抛出
        throw new BizException(result.getMsg());
    }

    /**
     * SSO-Client端：单点注销回调地址
     */
    @RequestMapping("/sso/logoutCall")
    public Object ssoLogoutCall(String loginId, String autoLogout, String timestamp, String nonce, String sign) {
        // 校验签名
        String calcSign = SsoRequestUtil.getLogoutCallSign(loginId, autoLogout, timestamp, nonce);
        if (!calcSign.equals(sign)) {
            log.error("无效签名，拒绝应答：" + sign);
            throw new BizException("无效签名，拒绝应答" + sign);
        }

        // 注销这个账号id
        return Result.ok(null, "账号id=" + loginId + " 注销成功");
    }

    /**
     * SSO-Client端：单点注销回调地址
     */
    @RequestMapping("/sso/pushC")
    public Object pushC(String loginId, String autoLogout, String timestamp, String nonce, String sign) {
        // 校验签名
        String calcSign = SsoRequestUtil.getLogoutCallSign(loginId, autoLogout, timestamp, nonce);
        if (!calcSign.equals(sign)) {
            log.error("无效签名，拒绝应答：" + sign);
            throw new BizException("无效签名，拒绝应答" + sign);
        }

        // 注销这个账号id
        return Result.ok(null, "账号id=" + loginId + " 注销成功");
    }

    /**
     * 查询我的账号信息
     * <p>
     * 调用此接口的前提是 sso-server 的 /sso/userinfo
     * </p>
     *
     * @return {@link Result }<{@link ? }>
     */
    @RequestMapping("/sso/userInfo")
    public SaResult userInfo(HttpServletRequest request) {
        // 组织 url 参数
        String satoken = request.getParameter("satoken");
        if (StrUtil.isEmpty(satoken)) {
            return SaResult.ok();
        }
        Object loginId = jwsTokenProvider.getTokenClaim(satoken, "SSO_ID"); // SSO中心用户id
        Object XTenantId = jwsTokenProvider.getTokenClaim(satoken, X_TENANT_ID); // 账号id
        String timestamp = String.valueOf(System.currentTimeMillis());    // 时间戳
        String nonce = SsoRequestUtil.getRandomString(20);        // 随机字符串
        String sign = SsoRequestUtil.getUserInfoSign(String.valueOf(XTenantId), loginId, timestamp, nonce);    // 参数签名

        String url = SsoRequestUtil.getDataUrl
                + "?msgType=userInfo"
                + "&loginId=" + loginId
                + "&timestamp=" + timestamp
                + "&nonce=" + nonce
                + "&sign=" + sign
                + "&client=sso-client1"
                + "&" + X_TENANT_ID + "=" + XTenantId;
        // 返回给前端
        return SsoRequestUtil.request(url);
    }

    /**
     * 全局异常拦截
     *
     * @param e e
     * @return {@link Result }<{@link ? }>
     */
    @ExceptionHandler
    public Result<?> handlerException(Exception e) {
        log.error("", e);
        return Result.fail(e.getMessage());
    }

}
